package com.example.java_doc_search.seacher;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// 通过这个类在内存中来构造出索引结构
public class Index {
    private static String INPUT_PATH = null;

    static {
        if (Config.isOnline) {
            INPUT_PATH = "";
        } else {
            INPUT_PATH = "E:\\Document java 8\\";
        }
    }

    // Jackson 里面核心的对象
    // 通过这个对象进行序列化和反序列化的操作
    private ObjectMapper objectMapper = new ObjectMapper();

    // 使用数组下标表示 docId (正排序）
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    // 使用 哈希表 表示倒排序
    // key 就是词
    // value 是一组和这个词有关联得文章
    private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();

    // 创建两个锁对象
    private Object locker1 = new Object();
    private Object locker2 = new Object();

    //这个类要提供的方法
    //1. 给定一个 docId，在正排索引中，查询文档的详细信息；
    public DocInfo getDocInfo(int docId) {
        // 需要自己创建文件详细信息，获得docId就可以获得对应的详细信息
        return forwardIndex.get(docId);
    }

    //2. 给定一个词，在倒序索引中，查那些文档和这个词关联；
    // term 是一个分词结果，而不是一句话去查
    // 单存返回一个List的整数是不可行的；
    // 因为词和文档之间是存在一定的“相关性”的，所以使用“weight"类来表达这种相关性。
    // seacher.Weight“相关性”会有大和小之分，怎样算之后再说
    public List<Weight> getInverted(String term) {
        return invertedIndex.get(term);
    }

    //3. 往索引中新增一个文档； 构建索引
    public void addDoc(String title, String url, String content) {
        // 新增文档操作，需要同时给正排索引和倒排索引新增信息
        // 构建正派索引
        DocInfo docInfo = bulidForward(title, url, content);
        // 构建倒排索引
        bulidInverted(docInfo);
    }

    private void bulidInverted(DocInfo docInfo) {
        // 使用内部类
        class WordCount {
            // 表示词在标题中出现的次数
            public int titleCount;
            // 表示词在正文中出现的次数
            public int contentCount;
        }
        // 用 HashMap 数据结构来统计词频
        // String: 出现的分词结果     WordCount：分词结果次数
        HashMap<String, WordCount> wordCntHashMap = new HashMap<>();

        //1. 针对文档标题进行分词；
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        //2. 遍历分词结果，统计每个词出现的次数；
        for (Term term : terms) {
            // 先判定分词是否存在
            String word = term.getName();
            WordCount wordCount = wordCntHashMap.get(word);
            if (wordCount == null) {
                // 不存在，创建一个新的键值对，插入进去，titleCount 设为 1；
                WordCount newWordCnt = new WordCount();
                newWordCnt.titleCount = 1;
                newWordCnt.contentCount = 0;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                // 存在，就找到之前的值，然后把对应的 titleCount +=1；
                wordCount.titleCount += 1;
            }
        }
        //3. 针对正文页进行分词；
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        //4. 遍历分词结果，统计每个词出现的次数
        for (Term term : terms) {
            // 与上面的情况同理
            String word = term.getName();
            WordCount wordCount = wordCntHashMap.get(word);
            if (wordCount == null) {
                WordCount newWordCnt = new WordCount();
                newWordCnt.contentCount = 1;
                newWordCnt.titleCount = 0;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                wordCount.contentCount += 1;
            }
        }
        //5. 把上面的结果汇总在一个 HashMap 中
        //6、遍历刚刚这个HashMap，依次来更新倒排索引中的结构了

        for (Map.Entry<String, WordCount> entry : wordCntHashMap.entrySet()) { //entrySet()一个整体的打印方式
            // 先根据这里的词，去倒排索引中查一查
            // 倒排拉链
            synchronized (locker1) {
                ArrayList<Weight> invertedList = invertedIndex.get(entry.getKey());
                if (invertedList == null) {
                    // 如果为 null 就插入一个新键值对
                    // 把新的文档（当前 seacher.DocInfo），构造成 seacher.Weight 对象，插入进来
                    ArrayList<Weight> newInvertedList = new ArrayList<>();
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    // 最终文档的权重，就设定成标题中出现的次数*10+正文出现的次数；
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    newInvertedList.add(weight);
                    invertedIndex.put(entry.getKey(), newInvertedList);
                } else {
                    // 如果非空，就把当前的文档，构造出一个weight 对象，插入到倒排拉链的后面
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    // 权重计算公式：标题中出现的次数*10+正文出现的次数；
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    invertedList.add(weight);

                }
            }

        }
    }

    private DocInfo bulidForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        synchronized (locker2) {
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }

    //4. 把内存中的索引结构保存到磁盘中；
    public void saveDisk() {
        long begin = System.currentTimeMillis();
        // 使用两个文件，分别保存正排和倒排 （先指定文件保存的目录）
        System.out.println("保存索引开始！");
        // 1、先判断以下索引对应的目录是否存在，不存在就创建
        File indexPathFile = new File(INPUT_PATH);
        if (!indexPathFile.exists()) { // 不存在就创建
            indexPathFile.mkdirs(); // 带“s” 可以创建多级目录
        }
        //创建文件保存正排和倒排
        File forwardIndexFile = new File(INPUT_PATH + "forward.txt");
        File invertedIndexFile = new File(INPUT_PATH + "inverted.txt");
        // objectMapper.writeValue(指定写在哪个文件里，对应那个对象进行写作）
        try {
            objectMapper.writeValue(forwardIndexFile, forwardIndex);
            objectMapper.writeValue(invertedIndexFile, invertedIndex);

        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("保存索引完成！！消耗时间：" + (end - begin) + "ms");

    }

    //5. 把磁盘中的索引结构加载到内存中
    public void load() {
        long begin = System.currentTimeMillis(); // 这里计算的是 ms
        System.out.println("加载索引开始！");
        // 1、先设置一下加载索引的路径
        File forwardIndexFile = new File(INPUT_PATH + "forward.txt");
        File invertedIndexFile = new File(INPUT_PATH + "inverted.txt");
        // objectMapper.readValue(从哪个文件读，读到的数据安照什么类型进行解析）
        try {
            // 这一步是重点！！！
            forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {
            });
            invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("加载索引结束！！！消耗的时间：" + (end - begin) + "ms");
    }

    public static void main(String[] args) {
        Index index = new Index();
        index.load();
        System.out.println("索引加载完毕！");
    }
}
